<?php
namespace App\Utils\Other\Jwt;

use App\Constants\ErrorCode;
use App\Exception\InvalidRequestException;
use App\Request\FormRequest;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Hyperf\Di\Annotation\Inject;
use Psr\SimpleCache\CacheInterface;
use function Hyperf\Config\config;
use function Hyperf\Translation\__;
use function Hyperf\Support\make;

Class Token {

    /**
     * @var CacheInterface
     */
    #[Inject]
    protected CacheInterface $cache;

    /**
     * 公钥过期时间
     * @var int
     */
    protected int $publicExp = 30*86400;

    /**
     * 生成token
     * @param mixed $data
     * @param int|null $exp
     * @return string
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function encode(mixed $data, ?int $exp=null)
    {
        $token = $this->token($data, $exp);

        $refreshToken = $this->token($data, $this->publicExp); //
        $this->cache->set(md5($token), $refreshToken, $this->publicExp);
        return config('jwt.prefix').' '.$token;
    }

    /**
     * 解密
     * @param $token
     * @return []
     * @throws InvalidRequestException()
     */
    public function decode(string $token, ?string &$newToken=null)
    {
        try {
            $decode = (array)JWT::decode($token, new Key(config('jwt.secret'), 'HS256'));
            $result = (array)$decode['data'];
        } catch (ExpiredException $expiredException) {
            $result = $this->refresh($token, $newToken);
        } catch (\Throwable $throwable) {
            throw new InvalidRequestException(__('message.jwt.failed'));
        }
        return $result;
    }

    /**
     * 重新获取token
     * @param $token
     * @param null $exp
     * @return array
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function refresh(string $token, ?string &$newToken=null):array
    {
        $whiteName = 'white_list_'.md5($token); // 白名单
        if ($this->cache->has($whiteName)) {
            $newToken = $this->cache->get($whiteName);
            $decode = (array)JWT::decode($newToken, new Key(config('jwt.secret'), 'HS256')); // 解密
            $data = (array)$decode['data'];
            return $data;
        }
        if (!$this->cache->has(md5($token))) {
            logs('cache not has');
            throw new InvalidRequestException(__('message.jwt.failure'), ErrorCode::SERVER_UNAUTHORIZED);
        }
        $refresh = $this->cache->get(md5($token));
        if (!$refresh || !$this->cache->has(md5($refresh))) {
            logs('refresh cache not has');
            throw new InvalidRequestException(__('message.jwt.failure'), ErrorCode::SERVER_UNAUTHORIZED);
        }
        try {
            $decode = (array)JWT::decode($refresh, new Key(config('jwt.secret'), 'HS256')); // 解密
            $data = (array)$decode['data'];
            $newToken = $this->token($data);
            $this->cache->set(md5($newToken), $refresh, $this->publicExp);
            $this->cache->set($whiteName, $newToken, 5); // 添加原始token到白名单, 5s后过期
            $this->cache->set(md5($token), $refresh, 5); // 清除当前登录token
            return $data;
        } catch (\Throwable $throwable) {
            throw new InvalidRequestException(__('message.jwt.failed'));
        }
    }

    /**
     * 生成token
     * @param mixed $data
     * @param int|null $exp
     * @return string
     */
    public function token(mixed $data, int $exp=null):string
    {
        $time = time();
        $request = make(FormRequest::class);

        $payload = [
            'iss' => config('app_name'),
            'exp' => $exp?$exp+$time:$time+config('jwt.refresh_exp'), // 默认十秒钟后过期
            'aud' => $request->domain(),
            'nbf' => $time,
            'iat' => $time,
            'data' => $data
        ];

        return JWT::encode($payload, config('jwt.secret'), 'HS256');
    }

    /**
     * 删除token
     * @param string $token
     * @return bool
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function delete(string $token):bool
    {
        if (!$this->cache->has(md5($token))) {
            return true;
        }
        $refresh = $this->cache->get(md5($token));
        $this->cache->delete(md5($token));
        $this->cache->delete(md5($refresh));
        return true;
    }
}